package org.fastiot

import grails.converters.JSON
import grails.databinding.BindUsing
import org.grails.web.converters.exceptions.ConverterException
import org.grails.web.json.JSONArray

class MetaField {

    static enum BindDomain {
        Terminal("org.fastiot.Terminal"), Model("org.fastiot.Model"), Group("org.fastiot.Group")
        final String domainName

        private BindDomain(String domainName) { this.domainName = domainName }

        static BindDomain byDomainName(String name) {
            values().find { it.domainName == name } ?:
                    { throw new IllegalArgumentException("MetaField\$BindDomain ${name} not found") }()
        }
    }

    static enum Preset {
        Download(0), Upload(1), None(9)
        final int value

        private Preset(int value) { this.value = value }

        static Preset byValue(int val) {
            values().find { it.value == val } ?:
                    { throw new IllegalArgumentException("MetaField\$Preset ${val} not found") }()
        }
    }

    static class Template {
        static enum InputForm {
            Text(0), Select(1), Radio(2), Check(3), Date(4)
            final int value

            private InputForm(int value) { this.value = value }

            static InputForm byValue(int val) {
                values().find { it.value == val } ?:
                        { throw new IllegalArgumentException("MetaField\$Template\$InputForm ${val} not found") }()
            }
        }

        static enum SelectSource {
            LocalJSON(0), NetworkJSON(1)
            final int value

            private SelectSource(int value) { this.value = value }

            static InputForm byValue(int val) {
                values().find { it.value == val } ?:
                        { throw new IllegalArgumentException("MetaField\$Template\$SelectSource ${val} not found") }()
            }
        }

        @BindUsing({ obj, source ->
            InputForm.byValue(Integer.parseInt(source["inputForm"] as String))
        })
        InputForm inputForm

        @BindUsing({ obj, source ->
            SelectSource.byValue(Integer.parseInt(source["sourceType"] as String))
        })
        SelectSource sourceType

        String sourceDefinition
        Boolean nullable

        static constraints = {
            inputForm(blank: false, inList: Template.InputForm.values().toList())
            sourceType(nullable: true, inList: Template.SelectSource.values().toList(), validator: Template.InputFormSelectBasicValidator)
            sourceDefinition(nullable: true, validator: { Object val, Template obj, org.springframework.validation.Errors errors ->
                Template.InputFormSelectBasicValidator(val, obj, errors)
                Template.InputFormSelectSourceDefinitionValidator(val, obj, errors)
            })
            nullable(nullable: false)
        }

        static InputFormSelectBasicValidator = { Object val, Template obj, org.springframework.validation.Errors errors ->
            if (obj.inputForm != InputForm.Select && val)
                errors.rejectValue('inputForm', 'onlySelectRequiresSources')
            else if (obj.inputForm == InputForm.Select && val == null)
                errors.rejectValue('inputForm', 'selectMustSetSources')
        }

        static InputFormSelectSourceDefinitionValidator = { Object val, Template obj, org.springframework.validation.Errors errors ->
            if (obj.sourceType != SelectSource.LocalJSON && val && Template.isValidJSONArray(val))
                errors.rejectValue('sourceDefinition', 'invalidLocalJSON')
            else if (obj.sourceType == SelectSource.NetworkJSON && val && /(http|ftp|https):\\/\\/([\w.]+\\/?)\S*/.matches(val))
                errors.rejectValue('sourceDefinition', 'invalidNetworkURL')
        }

        static isValidJSONArray(String jsonStr) {
            try {
                JSON.parse(jsonStr) instanceof JSONArray
            } catch (ConverterException ex) {
                false
            }
        }

        static mapping = {
            inputForm enumType: "ordinal"
            sourceType enumType: "ordinal"
        }
    }

    @BindUsing({ obj, source ->
        BindDomain.byDomainName(source["bind"] as String)
    })
    BindDomain bind

    String bindCodes

    String code
    String label

    @BindUsing({ obj, source ->
        Preset.byValue(Integer.parseInt(source["preset"] as String))
    })
    Preset preset = Preset.None

    String subcategory
    int layoutIndex
    Template template

    boolean enabled
    Date dateCreated
    Date lastUpdated

    static embedded = ['template']

    static constraints = {
        bind(nullable: false, inList: MetaField.BindDomain.values().toList())
        bindCodes(nullable: true, matches: /([a-zA-Z0-9]+[,]?)+/)
        code(blank: false, unique: "bind")
        label(blank: false)
        preset(nullable: false, inList: MetaField.Preset.values().toList())
        subcategory(nullable: true)
        layoutIndex(min: 1)
        template(nullable: false)
        enabled()
        dateCreated()
        lastUpdated()
    }

    static mapping = {
        bind enumType: "ordinal"
    }
}
